// board.go - Functions for displaying browsable pages of the board.

package main

import (
    "./cache"
    "./query"
    "config"
    "database/sql"
    "encoding/base64"
    "errors"
    "html/template"
    "io"
    "net/http"
    "path"
    "sort"
    "strconv"
    "time"
    "yotsuba"
)

// Get board listing for header and footer
func boardList() []string {
    var names []string
    for _, b := range config.Boards {
        names = append(names, b.Abbrev)
    }
    sort.Strings(names)
    return names
}

func errorPage(w http.ResponseWriter, tmpl *template.Template,
    title string, msg string) {
    type ErrorMsg struct {
        Title string
        Msg   string
    }

    tmpl.ExecuteTemplate(w, "error", ErrorMsg{title, msg})
}

// Link redirection to point browsers back to site, mainly to drop referal info
// while getting full-size images. Tried it with the Location field instead of
// Refresh and with Go's http.Redirect, but it triggered leech image responses
// after multiple image links for some reason.
func redirect(w http.ResponseWriter, r *http.Request) {
    encodedURL := path.Base(r.URL.String())
    link, _ := base64.URLEncoding.DecodeString(encodedURL)
    w.Header().Set("Refresh", "0; url="+string(link))
    w.WriteHeader(http.StatusSeeOther)
}

// Serve the full content of a thread.
func fullThread(w http.ResponseWriter, r *http.Request,
    tmpl *template.Template, board *config.Board) (
    cache.PageBuilder, time.Duration) {

    start := time.Now() // Begin clocking page construction.
    threadNo, err := strconv.ParseInt(path.Base(r.URL.Path), 10, 64)
    if err != nil {
        http.NotFound(w, r)
        return nil, 0.0
    }
    rows, e := query.Get(board.TableName, "fullThread").Query(threadNo)
    if e != nil {
        http.NotFound(w, r)
        return nil, 0.0
    }

    posts := []*yotsuba.SqlPost{}
    t := new(yotsuba.SqlThread)
    for rows.Next() {
        p, tTemp, _ := buildPost(board, rows)
        t = tTemp
        posts = append(posts, p)
    }

    if len(posts) < 1 {
        http.NotFound(w, r)
        return nil, 0.0
    }

    t.Head = posts[0]
    t.Tail = posts[1:]
    t.OpIntId = t.Head.PostId
    t.FullThread = true

    finish := time.Now()
    pageData := &unprocessedPage{
        BoardName:   board.Abbrev,
        Boards:      boardList(),
        Title:       board.Title,
        Generated:   finish,
        TimeElapsed: int64(finish.Sub(start).Seconds() * 1000),
        Threads:     []*yotsuba.SqlThread{t},
    }
    return func(wh io.Writer) error {
        return tmpl.ExecuteTemplate(wh, "thread", pageData)
    }, board.ThreadCacheTime
}

// Top page (thread summary listing) for a given board.
func boardTop(w http.ResponseWriter, r *http.Request, tmpl *template.Template,
    board *config.Board) (cache.PageBuilder, time.Duration) {

    start := time.Now() // Begin clocking page construction.
    r.ParseForm()
    var ghostMode bool
    if _, ok := r.Form["ghost"]; ok {
        ghostMode = true
    }

    pageNo, _ := strconv.Atoi(path.Base(r.URL.Path))
    rows, threadCount, e := runTopPageQuery(pageNo, ghostMode, board)
    if e != nil {
        errorPage(w, tmpl, e.Error(), e.Error())
    }
    threads := structureTopPageRows(rows, board)
    finish := time.Now() // Stop timing page construction.

    // Build page to serve through template.
    pageData := &unprocessedPage{
        Boards:  boardList(),
        Threads: threads,
        Pages: pageRange(threadCount, board.ThreadsPerPage,
            board.NumberOfPages, pageNo),
        PageNo:      pageNo,
        PageBase:    "/" + board.Abbrev + "/",
        NearEnd:     int(int(threadCount)/board.ThreadsPerPage) == pageNo,
        BoardName:   board.Abbrev,
        Title:       board.Title,
        TimeElapsed: int64(finish.Sub(start).Seconds() * 1000),
        Generated:   finish,
    }

    return func(wh io.Writer) error {
        return tmpl.ExecuteTemplate(wh, "board", pageData)
    }, board.BoardPageCacheTime
}

// Split rows from top page query into individual threads.
func structureTopPageRows(
    rows *sql.Rows, b *config.Board) []*yotsuba.SqlThread {

    threads := []*yotsuba.SqlThread{}
    var p, pOld *yotsuba.SqlPost
    var tTemp *yotsuba.SqlThread
    var images int64
    t := new(yotsuba.SqlThread)
    floor := func(a int64) int64 {
        if a > 0 {
            return a
        }
        return 0
    }

    for rows.Next() {
        p, tTemp, _ = buildPost(b, rows)

        // Initialize first thread if just starting, 
        // else finalize previous thread if boundary detected
        if pOld == nil {
            t = tTemp
        } else if pOld.Parent.Int64 != p.Parent.Int64 {
            t.OpIntId.Int64 = pOld.Parent.Int64

            t.PostsOmitted = sql.NullInt64{
                floor(t.PostCount.Int64 - int64(b.TailLength) - 1), true,
            }
            t.ImgsOmitted = sql.NullInt64{
                floor(t.ImgCount.Int64 - images), true,
            }
            threads = append(threads, t)
            t = new(yotsuba.SqlThread)
            t = tTemp
            images = 0
        }

        if p.Img != nil {
            images++
        }

        // Commit current post to current thread.
        if p.PostId.Int64 == p.Parent.Int64 {
            t.Head = p
        } else {
            t.Tail = append(t.Tail, p)
        }
        pOld = p
    }
    return threads
}

// Query board post count, check that page exists, query board rows. 
func runTopPageQuery(page int, ghostMode bool, board *config.Board) (
    *sql.Rows, int64, error) {
    min := page * board.ThreadsPerPage
    max := min + board.ThreadsPerPage
    bname := board.TableName

    row := query.Get(bname, "boardPostCount").QueryRow(bname + "_threads")
    var postCountSql sql.NullInt64
    e := row.Scan(&postCountSql)
    if e != nil {
        return nil, 0, errors.New("Could not get board post count")
    }
    threadCount := postCountSql.Int64
    if int(int(threadCount)/board.ThreadsPerPage) < page {
        return nil, 0, errors.New("Thread not found")
    }

    var rows *sql.Rows
    if ghostMode {
        rows, e = query.Get(bname, "ghostTopPageStmt").Query(
            min, max-min, board.TailLength)
    } else {
        rows, e = query.Get(bname, "topPageStmt").Query(
            min, max-min, board.TailLength)
    }
    if e != nil {
        // Just give up on bad connection. Shouldn't get here after cacheing is
        // implemented, though.
        return nil, 0, errors.New("Could not run top page query")
    }
    return rows, threadCount, nil
}
